#!/usr/bin/env ruby
# -*- coding: utf-8; mode: ruby; tab-width: 2; indent-tabs-mode: nil -*-
# $Id: fliewr.rb 2009/02/08 21:48:53 EET paul Exp $
#
=begin
  Flickr Viewr - Flicker Friends Photostream Viewer.

  Copyright (c) 2008, Paul Philippov <paul@ppds.ws>

  This software is released under the BSD License.
  http://creativecommons.org/licenses/BSD/
=end

### THERE NO NEED TO EDIT BELOW THIS LINE UNTIL YOU KNOW WHAT YOU DO ###

require 'gtk2'
require 'gconf2'
require 'gtkmozembed'
require 'net/http'
require 'xmlsimple'

SECONDS_PER_MINUTE = 60

begin
  def load_resource(file)
    File.open(File.join('resource',file), "r") { |f| f.read }
  end
  HTML = load_resource 'theme.html'
  ITEM = load_resource 'item.html'
  ERRR = load_resource 'error.html'
  CSS  = load_resource 'style.css'
  SVG  = load_resource 'icon.svg'
  URL  = load_resource 'api.url'
  puts "Running with external resources"
rescue
  SVG, URL, CSS, HTML, ITEM, ERRR = DATA.read.unpack('m')[0].split("\n")
end

class StatusIcon < Gtk::StatusIcon
  def initialize(window)
    mi_profile = Gtk::ImageMenuItem.new(Gtk::Stock::HOME)
    mi_profile.signal_connect(:activate) do
      window.visit_user_profile
    end

    mi_update = Gtk::ImageMenuItem.new(Gtk::Stock::REFRESH)
    mi_update.signal_connect(:activate) do
      window.update
    end

    mi_on_top = Gtk::CheckMenuItem.new('Always on top')
    mi_on_top.signal_connect(:toggled) do
      window.keep_above = mi_on_top.active?
    end

    mi_config = Gtk::ImageMenuItem.new(Gtk::Stock::PREFERENCES)
    mi_config.signal_connect(:activate) do
      window.configure
    end

    mi_quit = Gtk::ImageMenuItem.new(Gtk::Stock::QUIT)
    mi_quit.signal_connect(:activate) do
      window.quit
    end

    mi_about = Gtk::ImageMenuItem.new(Gtk::Stock::ABOUT)
    mi_about.signal_connect(:activate) do
      AboutDialog.new
    end

    menu = Gtk::Menu.new
    [ mi_profile, mi_update, separator, mi_on_top, separator, mi_config,
      separator, mi_about, separator, mi_quit ].each { |mi| menu.append mi }

    menu.show_all
    super()
    set_pixbuf  FlickrViewr::APP_ICON
    set_tooltip FlickrViewr::APP_NAME

    signal_connect(:activate) do
      window.toggle_visibility
    end

    signal_connect(:popup_menu) do |tray, button, time|
      menu.popup nil, nil, button, time
    end
  end

  def separator
    Gtk::SeparatorMenuItem.new
  end
end

def icon_from_stream(data)
  icon = Gdk::PixbufLoader.new
  icon.last_write(data)
  icon.pixbuf
end

class AboutDialog
  def initialize
    Gtk::AboutDialog.set_email_hook { |widget, email| system("xdg-email #{email}") }
    Gtk::AboutDialog.set_url_hook { |widget, link| system("xdg-open #{link}") }
    dialog = Gtk::AboutDialog.new
    dialog.name = FlickrViewr::APP_NAME
    dialog.program_name = FlickrViewr::APP_NAME
    dialog.comments = FlickrViewr::APP_COMMENTS
    dialog.version = FlickrViewr::APP_VERSION.to_s
    dialog.copyright = FlickrViewr::APP_COPYRIGHT
    dialog.license = FlickrViewr::APP_LICENSE
    dialog.authors = FlickrViewr::APP_AUTHORS
    dialog.documenters = FlickrViewr::APP_DOCUMENTERS
    dialog.website = FlickrViewr::APP_WEBSITE
    dialog.logo = FlickrViewr::APP_ICON.add_alpha(true, 0xff, 0xff, 0xff)
    dialog.run
    dialog.destroy
  end
end

class FlickrViewr < Gtk::Window
  APP_ID = 'fliewr'
  APP_NAME = 'Flickr Viewr'
  APP_VERSION = 1.0
  APP_COMMENTS = "Flickr photostream viewer"
  APP_COPYRIGHT = "Copyright (c) #{Time.now.year}, Paul Philippov"
  APP_TITLE = "#{APP_NAME} v.#{APP_VERSION}"
  APP_AUTHORS = ['Paul Philippov <paul@ppds.ws>']
  APP_DOCUMENTERS = ['Paul Philippov <paul@ppds.ws>']
  APP_WEBSITE = "http://fliewr.googlecode.com/"
  APP_LICENSE = "
    This software is released under the BSD License.
    http://creativecommons.org/licenses/BSD/
  "
  APP_ICON = icon_from_stream(SVG)

  APP_GCONF_KEY = '/apps/' + APP_ID
  APP_MIN_WIDTH = 278

  NSID_MASK = /\d\d\d\d\d\d\d\d@N\d\d/
  FLICKR = 'http://www.flickr.com/'
  PROFILE_URL_TEMPLATE = FLICKR + 'people/%s'
  USERICON_NOTFOUND = FLICKR + 'images/buddyicon.jpg'
  USERICON_TEMPLATE = 'http://farm%s.static.flickr.com/%s/buddyicons/%s.jpg'
  NSID_HELP = FLICKR + 'services/api/explore/?method=flickr.people.getInfo'

  def initialize
    super Gtk::Window::TOPLEVEL
    signal_connect(:destroy) { quit }
    signal_connect(:delete_event) { minimize }
    signal_connect(:window_state_event) { |w, event| cb_window_state(event) }
    signal_connect(:check_resize) { remember_size_position }
    set_title APP_NAME
    set_icon APP_ICON
    set_width_request APP_MIN_WIDTH
    set_allow_shrink false

    init_config

    set_default_size @size_w, @size_h
    move @pos_x, @pos_y

    @progressbar = Gtk::ProgressBar.new

    @statusbar = Gtk::Statusbar.new
    @statusbar.has_resize_grip = false

    @toolbar = Gtk::Toolbar.new
    @toolbar.icon_size = Gtk::IconSize::MENU
    @toolbar.insert 0, toolbutton_with_click(:home, :visit_user_profile)
    @toolbar.insert 1, toolbutton_with_click(:refresh, :ask_update, true)
    @toolbar.insert 2, Gtk::ToolItem.new.add(@progressbar)
    @toolbar.insert 3, toolbutton_with_click(:preferences, :configure)
    @toolbar.insert 4, Gtk::SeparatorToolItem.new
    @toolbar.insert 5, toolbutton_with_click(:quit, :quit)

    @browser = Gtk::MozEmbed.new
    @browser.signal_connect(:open_uri) do |w, uri|
      open_in_external_browser(uri)
    end

    vbox = Gtk::VBox.new false, 4
    vbox.pack_start @toolbar, false, false
    vbox.pack_start @browser
    vbox.pack_start @statusbar, false, false
    self.add vbox

    @default.can_default = true
    @default.can_focus = true
    set_default @default
    set_focus @default

    ask_update
    start_timer
  end

  def remember_size_position
    @pos_x, @pos_y = position
    @size_w, @size_h = size
  end

  def cb_window_state(event)
    case event.event_type
    when Gdk::Event::WINDOW_STATE
      minimize if event.changed_mask.iconified? and event.new_window_state.iconified?
    end
  end

  def toggle_visibility
    visible? ? minimize : maximize
  end

  def minimize
    remember_size_position
    hide
  end

  def maximize
    move @pos_x, @pos_y
    show
    present
  end

  def tick
    sec_to_next_update = @requires_update_at - timestamp
    if sec_to_next_update < 0
      update
    else
      full_gauge = @requires_update_at - @last_updated_at
      sec_since_last_update = timestamp - @last_updated_at
      fraction = (sec_since_last_update / full_gauge)
      @progressbar.fraction = fraction
      @progressbar.text = "%s to update" % human_time(sec_to_next_update)
    end
  end

  def update
    stop_timer
    Thread.new do
      ask_update @update_interval * 60
      url = URL % [
        @nsid,
        @max_photos,
        @one_per_contact ? 1 : 0,
        @only_from_friends ? 1 : 0,
        @include_self ? 1 : 0
      ]
      xml = Net::HTTP.get_response(URI.parse(url)).body
      doc = XmlSimple.xml_in(xml)
      raise doc['err'][0]['msg'] if doc['err']
      refresh_with doc['photos'][0]['photo']
      @last_updated_at = timestamp
      @statusbar.push 0, "The photostream of #{@nsid} with friends"
    end
    start_timer
  rescue => e
    alert @nsid, e.message, e.backtrace
  end

  def ask_update(time_ahead = 0)
    @requires_update_at = timestamp + time_ahead
  end

  def start_timer
    @@timeout = Gtk.timeout_add(1000) { tick }
  end

  def stop_timer
    Gtk.timeout_remove(@@timeout) if @@timeout
    @progressbar.fraction = 1
    @progressbar.text = "updating..."
  end

  def timestamp
    Time.now.to_i
  end

  def human_time(time)
    seconds = time.modulo(SECONDS_PER_MINUTE)
    minutes = (time - seconds) / SECONDS_PER_MINUTE
    "%d:%02d" % [minutes, seconds]
  end

  def icon(stock_id)
    Gtk::Stock.const_get stock_id.to_s.upcase
  end

  def label(text)
    Gtk::Label.new(text).set_alignment(0, 0.5)
  end

  def toollabel(text)
    Gtk::ToolItem.new.add label(text)
  end

  def button_with_click(stock_id, method_name)
    button = Gtk::Button.new icon(stock_id)
    button.signal_connect(:clicked) { self.method(method_name).call }
    button.show_all
  end

  def toolbutton_with_click(stock_id, method_name, default = false)
    button = Gtk::ToolButton.new icon(stock_id)
    button.signal_connect(:clicked) { self.method(method_name).call }
    @default = button if default
    button.show_all
  end

  def open_in_gecko(html)
    @browser.open_stream "file:///", "text/html"
    @browser.append_data HTML % [ CSS, html ]
    @browser.close_stream
  end

  def refresh_with(data)
    open_in_gecko data.map { |d| templatize(d) }.join
  end

  def alert(*args)
    open_in_gecko ERROR % [args].flatten.map { |e| "<li>#{e}</li>" }.join
    false
    end

  def templatize(data)
    ITEM % [
      photolink(data['owner'],  data['id']),
      photopreview(data['farm'], data['server'], data['id'], data['secret']),
      userprofile(data['owner']),
      data['username'],
      userpic(data['iconfarm'], data['iconserver'], data['owner']),
      data['title'],
      dateupload(data['dateupload'])
    ]
  end

  def photolink(*args)
    'http://www.flickr.com/photos/%s/%s' % args
  end

  def photopreview(*args)
    'http://farm%s.static.flickr.com/%s/%s_%s_m.jpg' % args
  end

  def userprofile(id)
    'http://www.flickr.com/people/%s' % id
  end

  def userpic(farm, server, owner)
    return USERICON_NOTFOUND if farm.to_i == 0
    USERICON_TEMPLATE % [ farm, server, owner ]
  end

  def dateupload(date_and_time)
    Time.at(date_and_time.to_i)
  end

  def configure
    dialog = Gtk::Dialog.new "Settings", nil, Gtk::Dialog::MODAL,
      [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT],
      [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL]

    dialog.set_default_response Gtk::Dialog::RESPONSE_ACCEPT
    dialog.signal_connect :response do |dialog, response|
      case response
      when Gtk::Dialog::RESPONSE_ACCEPT
        if NSID_MASK =~ @nsid_entry.text
          @nsid = @nsid_entry.text
          @update_interval = @update_interval_spin.value
          @max_photos = @max_photos_spin.value
          @one_per_contact = @one_per_contact_toggle.active?
          @only_from_friends = @only_from_friends_toggle.active?
          @include_self = @include_self_toggle.active?
          update
        else
          alert("User ID has incorrect format")
        end
      when Gtk::Dialog::RESPONSE_CANCEL
        true
      end
    end
    hbox = Gtk::HBox.new(false, 8)
    hbox.set_border_width(8)

    dialog.vbox.pack_start(hbox, false, false, 0)
    stock = Gtk::Image.new(Gtk::Stock::PREFERENCES, Gtk::IconSize::DIALOG)
    hbox.pack_start(stock, false, false, 0)

    table = Gtk::Table.new(3, 6)
    table.set_row_spacings(4)
    table.set_column_spacings(8)
    hbox.pack_start(table, true, true, 0)

    @nsid_entry = Gtk::Entry.new
    @nsid_entry.width_chars = 13
    @nsid_entry.max_length = 12
    @nsid_entry.text = @nsid
    table.attach_defaults label('Your Flickr user ID'), 0, 1, 0, 1
    table.attach_defaults @nsid_entry, 1, 2, 0, 1
    table.attach_defaults button_with_click(:find, :find_nsid), 2, 3, 0, 1

    @update_interval_spin = Gtk::SpinButton.new(1, 100, 1)
    @update_interval_spin.value = @update_interval
    table.attach_defaults label('Check new photos every'), 0, 1, 1, 2
    table.attach_defaults @update_interval_spin, 1, 2, 1, 2
    table.attach_defaults label('minutes'), 2, 3, 1, 2

    @max_photos_spin = Gtk::SpinButton.new(1, 50, 1)
    @max_photos_spin.value = @max_photos
    table.attach_defaults label('Maximum photos to display'), 0, 1, 2, 3
    table.attach_defaults @max_photos_spin, 1, 2, 2, 3

    @one_per_contact_toggle = Gtk::CheckButton.new
    @one_per_contact_toggle.active = @one_per_contact
    table.attach_defaults label('Only one photo per contact'), 0, 1, 3, 4
    table.attach_defaults @one_per_contact_toggle, 1, 2, 3, 4

    @only_from_friends_toggle = Gtk::CheckButton.new
    @only_from_friends_toggle.active = @only_from_friends
    table.attach_defaults label('Only from friends and family'), 0, 1, 4, 5
    table.attach_defaults @only_from_friends_toggle, 1, 2, 4, 5

    @include_self_toggle = Gtk::CheckButton.new
    @include_self_toggle.active = @include_self
    table.attach_defaults label('Include your own photos'), 0, 1, 5, 6
    table.attach_defaults @include_self_toggle, 1, 2, 5, 6

    dialog.show_all
    dialog.run
    dialog.destroy
  end

  def user_profile_link
    PROFILE_URL_TEMPLATE % @nsid
  end

  def visit_user_profile
    open_in_external_browser user_profile_link
  end

  def find_nsid
    open_in_external_browser(NSID_HELP)
  end

  def open_in_external_browser(uri)
    Thread.new { system('xdg-open "%s"' % uri) }
  end

  def init_config
    @@gconf = GConf::Client.default
    check_defaults
    load_config
  end

  def check_defaults
    @@gconf[key_from_name(:nsid)] ||= "43354049@N00"
    @@gconf[key_from_name(:pos_x)] ||= 0
    @@gconf[key_from_name(:pos_y)] ||= 0
    @@gconf[key_from_name(:size_w)] ||= 250
    @@gconf[key_from_name(:size_h)] ||= 500
    @@gconf[key_from_name(:update_interval)] ||= 5
    @@gconf[key_from_name(:last_updated_at)] ||= timestamp
    @@gconf[key_from_name(:requires_update_at)] ||= 0
    @@gconf[key_from_name(:max_photos)] ||= 20
    @@gconf[key_from_name(:one_per_contact)] ||= false
    @@gconf[key_from_name(:only_from_friends)] ||= false
    @@gconf[key_from_name(:include_self)] ||= false
  end

  def load_config
    @@gconf.all_entries(APP_GCONF_KEY).each do |entry|
      instance_variable_set_by_entry(entry)
    end
  end

  def save_config
    @@gconf.all_entries(APP_GCONF_KEY).each do |entry|
      @@gconf[entry.key] = instance_variable_get_by_entry(entry)
    end
    @@gconf.suggest_sync
  end

  def instance_variable_set_by_entry(entry)
    instance_variable_set '@' + name_from_key(entry.key), entry.value
  end

  def instance_variable_get_by_entry(entry)
    instance_variable_get '@' + name_from_key(entry.key)
  end

  def name_from_key(key)
    key.split('/').last
  end

  def key_from_name(name)
    APP_GCONF_KEY + '/' + name.to_s
  end

  def quit
    Gtk.main_quit
  ensure
    save_config
  end
end

Gtk::MozEmbed.set_profile_path ENV['HOME'] + '/.mozilla', FlickrViewr::APP_ID

w = FlickrViewr.new
s = StatusIcon.new(w)
Thread.new { s.show }
w.show_all

Gtk.main

# vim:set fenc=utf-8 ft=ruby et sw=2 ts=2 sts=2 nowrap

# BELOW ARE HTML TEMPLATES AND CSS. DO NOT DELETE!
__END__
PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRo
PSIxMjgiIGhlaWdodD0iMTI4Ij48cGF0aCBmaWxsPSIjMDA2M2RjIiBzdHJv
a2U9Im5vbmUiIGQ9Ik0gNjQuNTA0NDYxLDE2LjgxNzQ4NyBDIDM4LjE2ODUz
NiwxNy40ODgzMDQgMTYuOTk5Nzc4LDM5LjE3ODI1IDE2Ljk5OTc3OCw2NS43
ODcxMzEgQyAxNi45OTk3NzgsODQuMDE0Njc5IDI2Ljk0MzQxOCw5OS44OTg0
MDQgNDEuNjY2NDA0LDEwOC4zMjk1MSBDIDM3LjYxNzgzNywxMDEuMTkyMjUg
MzUuMjg1NDYyLDkyLjk0NzEwMiAzNS4yODU0NjIsODQuMTUwNzQ3IEMgMzUu
Mjg1NDYyLDU3LjExOTQ5OCA1Ny4xMzA3NTMsMzUuMTgxMTAzIDg0LjA0NzI4
NiwzNS4xODExMDMgQyA5Mi44MTM2MjUsMzUuMTgxMTAzIDEwMS4wMzE0NSwz
Ny41MTcxMTYgMTA4LjE0MjQ4LDQxLjU4OTI0MSBDIDk5Ljc0ODU1OSwyNi43
OTgyODQgODMuOTE1Njc4LDE2LjgxNzQ4NyA2NS43NjE2MDIsMTYuODE3NDg3
IEMgNjUuMzQxMDMyLDE2LjgxNzQ4NyA2NC45MjI0OTEsMTYuODA2ODM2IDY0
LjUwNDQ2MSwxNi44MTc0ODcgeiIgLz48cGF0aCBmaWxsPSIjZmYwMDg0IiBz
dHJva2U9Im5vbmUiIGQ9Ik0gOTQuODA5NzIzLDQ3LjMyMTE5MiBDIDk3Ljcx
NTM5Niw1Mi40MTk1NTcgOTkuMzg2MDMxLDU4LjMxNzU4NSA5OS4zODYwMzEs
NjQuNjAxODcgQyA5OS4zODYwMzEsODMuOTA1OTYgODMuNzE4OTQsOTkuNTcz
MDQ3IDY0LjQxNDg0OSw5OS41NzMwNDcgQyA1OC4xMjc3NzgsOTkuNTczMDQ3
IDUyLjIzNDA4NSw5Ny44OTExNTIgNDcuMTM0MTY3LDk0Ljk4MzA4IEMgNTMu
MTUzMjc1LDEwNS41NDkxOSA2NC41MDY3NzksMTEyLjY4NzI1IDc3LjUyOTA0
MSwxMTIuNjg3MjUgQyA5Ni44MzMxMzcsMTEyLjY4NzI1IDExMi41MDAyMiw5
Ny4wMjAxNjIgMTEyLjUwMDIyLDc3LjcxNjA2MSBDIDExMi41MDAyMiw2NC42
OTkwNDIgMTA1LjM2ODgyLDUzLjM0MjE3OSA5NC44MDk3MjMsNDcuMzIxMTky
IHoiIC8+PC9zdmc+Cmh0dHA6Ly9hcGkuZmxpY2tyLmNvbS9zZXJ2aWNlcy9y
ZXN0P21ldGhvZD1mbGlja3IucGhvdG9zLmdldENvbnRhY3RzUHVibGljUGhv
dG9zJmFwaV9rZXk9MDI4MmY5M2ViNmRhMDQyNTMxMGJkNTc5MWY0YzJmYzAm
ZXh0cmFzPWRhdGVfdXBsb2FkLGljb25fc2VydmVyLGxpY2Vuc2UmdXNlcl9p
ZD0lcyZjb3VudD0lZCZzaW5nbGVfcGhvdG89JWQmanVzdF9mcmllbmRzPSVk
JmluY2x1ZGVfc2VsZj0lZApib2R5IHsgcGFkZGluZyA6IDA7IGNvbG9yIDog
Izk5OTsgY29sb3IgOiAjZmZmOyBiYWNrZ3JvdW5kIDogIzMzMzsgbGluZS1o
ZWlnaHQgOiAxLjI1MDsgZm9udCA6IDEycHggIk5va2lhU2Fuc0NuIiwgIkx1
Y2lkYUdyYW5kZSIsIHNhbnMtc2VyaWZ9Ym9keSwgaW1nIHsgbWFyZ2luIDog
MH1pbWcgeyBib3JkZXIgOiBub25lfWRpdiwgaDEsIHAsIGEgeyBtYXJnaW4t
Ym90dG9tIDogMH1hIHsgY29sb3IgOiAjZmZmOyB0ZXh0LWRlY29yYXRpb24g
OiBub25lfWRpdi5lcnJvciB7IHBhZGRpbmcgOiAxMHB4fWRpdi5tZXNzYWdl
IHsgY2xlYXIgOiBib3RoOyBwb3NpdGlvbiA6IHJlbGF0aXZlOyBwYWRkaW5n
IDogN3B4OyBtYXJnaW4gOiA1cHg7IGNvbG9yIDogI2JiYjsgYmFja2dyb3Vu
ZC1jb2xvciA6ICM0NDQ7IC1tb3otYm9yZGVyLXJhZGl1cyA6IDdweH1hLmlt
YWdlIHsgZGlzcGxheSA6IGJsb2NrOyBoZWlnaHQgOiAxMzBweDsgYmFja2dy
b3VuZC1wb3NpdGlvbiA6IGNlbnRlcn1kaXYudXNlcnBpYyB7IHdpZHRoIDog
NTVweDsgaGVpZ2h0IDogMTAwJTsgZmxvYXQgOiBsZWZ0fWRpdi5pbWdib3gg
eyB3aWR0aCA6IDQ4cHg7IGhlaWdodCA6IDQ4cHg7IG1hcmdpbi1ib3R0b20g
OiA1cHg7IGJhY2tncm91bmQtcmVwZWF0IDogbm8tcmVwZWF0fWRpdi5wb3B1
cCB7IHBvc2l0aW9uIDogYWJzb2x1dGU7IGxlZnQgOiA3cHg7IHJpZ2h0IDog
N3B4OyBib3R0b20gOiA3cHg7IGJhY2tncm91bmQtY29sb3IgOiAjMDAwOyBw
YWRkaW5nIDogNXB4OyBkaXNwbGF5IDogbm9uZX1kaXYubWVzc2FnZTpob3Zl
ciBkaXYucG9wdXAgeyBkaXNwbGF5IDogaW5oZXJpdH1oMSB7IGZvbnQgOiBi
b2xkIDExcHggIk5va2lhU2FucyIsICJMdWNpZGFHcmFuZGUiLCBzYW5zLXNl
cmlmOyBtYXJnaW4tdG9wIDogMDsgaGVpZ2h0IDogMi41MDBlbTsgb3ZlcmZs
b3cgOiBoaWRkZW47IGNvbG9yIDogI2ZmMH1wLnRpbWUgeyBmb250LXNpemUg
OiAxMHB4OyB0ZXh0LWFsaWduIDogcmlnaHR9CjwhRE9DVFlQRSBodG1sIFBV
QkxJQyAiLS8vVzNDLy9EVEQgWEhUTUwgMS4wIFRyYW5zaXRpb25hbC8vRU4i
ICJodHRwOi8vd3d3LnczLm9yZy9UUi94aHRtbDEvRFREL3hodG1sMS10cmFu
c2l0aW9uYWwuZHRkIj48aHRtbCB4bWw6bGFuZz0iZW4iIHhtbG5zPSJodHRw
Oi8vd3d3LnczLm9yZy8xOTk5L3hodG1sIiBsYW5nPSJlbiI+PGhlYWQ+PHRp
dGxlPkZsaWNrciBWaWV3cjwvdGl0bGU+PG1ldGEgaHR0cC1lcXVpdj0iQ29u
dGVudC1UeXBlIiBjb250ZW50PSJ0ZXh0L2h0bWw7Y2hhcnNldD11dGYtOCIg
Lz48c3R5bGUgdHlwZT0idGV4dC9jc3MiPi8qPCFbQ0RBVEFbKi8lcy8qXV0+
Ki88L3N0eWxlPjwvaGVhZD48Ym9keT4lczwvYm9keT48L2h0bWw+CjxkaXYg
Y2xhc3M9Im1lc3NhZ2UiPjxhIGNsYXNzPSJpbWFnZSIgaHJlZj0iJXMiIHN0
eWxlPSJiYWNrZ3JvdW5kLWltYWdlOnVybCglcyk7Ij48L2E+PGRpdiBjbGFz
cz0icG9wdXAiPjxkaXYgY2xhc3M9InVzZXJwaWMiPjxhIGhyZWY9IiVzIiB0
aXRsZT0iJXMiPjxpbWcgc3JjPSIlcyIgYWx0PSIiIC8+PC9hPjwvZGl2Pjxo
MT4lczwvaDE+PHAgY2xhc3M9InRpbWUiPiVzPC9wPjwvZGl2PjwvZGl2Pgo8
ZGl2IGlkPSJlcnJvciI+IDxoMT5FcnJvcjwvaDE+ICVzPC9kaXY+
